#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Roxane Levy

from numpy import apply_along_axis,random,linalg,array,zeros,arange,sqrt,unravel_index,nditer


"""
   Classe "Som"
	Elle crée le réseau neuronal et gère l'apprentissage.
	
"""

class Som:
    def __init__(self,x,y,input_len,taux_apprentissage=1.0,diffusion="s"):

        self.taux_apprentissage = taux_apprentissage
	self.diffusion = diffusion
	
	# Ajustement des parametres selon la fonction de voisinage voulue     

	if (self.diffusion == "s"):
		self.sigma = max(x,y)/2
		self.neighborhood = self.simple

	if (self.diffusion == "g") :
		self.sigma = max(x,y)/2
		self.neighborhood = self.gaussian


	if (self.diffusion == "m") : 
		self.neighborhood = self.gaussian
		self.sigma = 1.0
		
	# initialisation des poids de la map
        self.weights = random.rand(x,y,input_len) 
        self.weights = array([v/linalg.norm(v) for v in self.weights]) # normalisation

        self.activation_map = zeros((x,y))

	self.neigx = arange(x)
        self.neigy = arange(y)



    def gaussian(self,c,sigma):
        d = 2*pi*sigma*sigma
        ax = exp(-power(self.neigx-c[0],2)/d)
        ay = exp(-power(self.neigy-c[1],2)/d)
        return outer(ax,ay)


    def simple(self,c,sigma) : 
	n = self.activation_map
	return n


    def euclidian(self, vectorMap, vectorData) :
	""" Fonction qui retourne la distance euclidienne entre deux vecteurs """
	distance = 0 
        for i in range(len(vectorMap)) :
 		distance += (vectorData[i] - vectorMap[i]) * (vectorData[i] - vectorMap[i])
	dist = sqrt(distance)
	return dist



    def findbestmatchingnode(self,vector) :
      for a in range(len(self.weights)) : 
	   for b in range(len(self.weights[a])) : 
		self.activation_map[a][b] = linalg.norm(self.euclidian(self.weights[a][b],vector))
      return unravel_index(self.activation_map.argmin(),self.activation_map.shape) 



    
    def update(self,x,win,t,sig):
	if (self.diffusion == "g") : 
		alpha = float(self.C) / float(self.C + t)
        	sig = self.sigma * (1.0 - float(t)/self.T) 
        	g = self.neighborhood(win,sig) 
        	it = nditer(g, flags=['multi_index'])
        	while not it.finished:
		  d = abs(win[0] - it.multi_index[0]) + abs(win[1] - it.multi_index[1])
		  if (d < sig) :
			  self.weights[it.multi_index] += alpha*(x-self.weights[it.multi_index])            
          	  self.weights[it.multi_index] = self.weights[it.multi_index] / linalg.norm(self.weights[it.multi_index])
          	  it.iternext()
	if (self.diffusion == "m") :
       		eta = self.taux_apprentissage/(1+t/self.T)
        	sig = self.sigma/(1+t/self.T) 
        	g = self.neighborhood(win,sig)*eta
        	it = nditer(g, flags=['multi_index'])
        	while not it.finished:
          	  self.weights[it.multi_index] += g[it.multi_index]*(x-self.weights[it.multi_index])            
          	  self.weights[it.multi_index] = self.weights[it.multi_index] / linalg.norm(self.weights[it.multi_index])
          	  it.iternext()
	if (self.diffusion == "s") :
       		alpha = 1.0 - float(t)/self.T
        	sig = self.sigma * (1.0 - float(t)/self.T) 
        	g = self.neighborhood(win,sig) 
        	it = nditer(g, flags=['multi_index'])
        	while not it.finished:
		  d = abs(win[0] - it.multi_index[0]) + abs(win[1] - it.multi_index[1])
		  if (d < sig) :
			  self.weights[it.multi_index] += alpha*(x-self.weights[it.multi_index])            
          	  self.weights[it.multi_index] = self.weights[it.multi_index] / linalg.norm(self.weights[it.multi_index])
          	  it.iternext()




    def distance_map(self):
        distance_map = zeros((self.weights.shape[0],self.weights.shape[1]))
        it = nditer(distance_map, flags=['multi_index'])
        while not it.finished:
            for ii in range(it.multi_index[0]-1,it.multi_index[0]+2):
                for jj in range(it.multi_index[1]-1,it.multi_index[1]+2):
                    if ii >= 0 and ii < self.weights.shape[0] and jj >= 0 and jj < self.weights.shape[1]:
                        distance_map[it.multi_index] += linalg.norm(self.weights[ii,jj,:]-self.weights[it.multi_index])
            it.iternext()
        distance_map = distance_map/distance_map.max()
        return distance_map





    def input_random(self,data,num_iteration):        
        """ Apprentissage du SOM en chargant les vecteurs de données dans le désordre """
	self._init_T(num_iteration) 
        for iteration in range(num_iteration):
            rand_i = int(round(random.rand()*len(data)-1))    
            self.update(data[rand_i],self.findbestmatchingnode(data[rand_i]),iteration+1,self.diffusion)



    def input_sequence(self,data,num_iteration):        
        """ Apprentissage du SOM en chargant les vecteurs de données en séquence """
	self._init_T(num_iteration) 
        for iteration in range(num_iteration):
            idx = iteration % (len(data)-1)             
            self.update(data[idx],self.findbestmatchingnode(data[idx]),iteration+1,self.diffusion)
	   



    def _init_T(self,num_iteration):
        """ Permet d'initialiser la variable Ttotal """
        if (self.diffusion == "m") : self.T = num_iteration/2
	if (self.diffusion == "s") : self.T = num_iteration
	if (self.diffusion == "g") : 
		self.T = num_iteration
		self.C = self.T / 100



if __name__ == '__main__':
	data = random.rand(100,3)
	data = apply_along_axis(lambda x: x/linalg.norm(x),1,data) # normalisation des données
	som = Som(5,5,3)
	som.input_random(data,100)
 

	


